/**
 * Copyright (c) 2010, Todd Ginsberg
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 *    * Redistributions of source code must retain the above copyright
 *      notice, this list of conditions and the following disclaimer.
 *    * Redistributions in binary form must reproduce the above copyright
 *      notice, this list of conditions and the following disclaimer in the
 *      documentation and/or other materials provided with the distribution.
 *    * Neither the name of Todd Ginsberg, or Gowalla nor the
 *      names of any contributors may be used to endorse or promote products
 *      derived from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * 
 *  Also, please use this for Good and not Evil.  
 */
package com.ginsberg.gowalla.request;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;

import com.ginsberg.gowalla.auth.Authentication;
import com.ginsberg.gowalla.exception.GowallaRequestException;
import com.ginsberg.gowalla.exception.NotAuthorizedException;
import com.ginsberg.gowalla.exception.RateLimitExceededException;
import com.ginsberg.gowalla.exception.RequestNotAcceptableException;
import com.ginsberg.gowalla.exception.RequestNotFoundException;
import com.ginsberg.gowalla.exception.ServiceUnavailableException;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonParseException;

/**
 * The main implementation of RequestHandler.  This implementation uses
 * the basic Java UrlConnection to make its requests.  It doesn't reuse
 * connections (HTTP/1.1).  This is a very basic implementation used to
 * provide functionality out of the box and provide the bare minimum
 * required to make requests.  
 * 
 * A more robust implementation could probably be written with Apache
 * Commons HTTP Client, should you so desire one.
 * 
 * @author Todd Ginsberg
 */
public class DefaultRequestHandler implements RequestHandler {
	
	private String host = "api.gowalla.com";
	private Collection<RequestHeader> headers = Collections.emptyList();
	private Gson gson;

	public DefaultRequestHandler() {
		super();
	}

	@Override
	public void setRequestHost(final String host) {
		this.host = host;
	}

	/**
	 * @see com.ginsberg.gowalla.request.RequestHandler#setRequestHeaders(java.util.Collection)
	 */
	@Override
	public void setRequestHeaders(final Collection<RequestHeader> headers) {
		this.headers = new LinkedList<RequestHeader>(headers);
	}
	
	/**
	 * 
	 * @see com.ginsberg.gowalla.request.RequestHandler#postResource(java.lang.String, com.ginsberg.gowalla.auth.Authentication, java.util.Collection)
	 */
	@Override
	public String postResource(final String resource, final Authentication authentication, final Collection<RequestParameter> parameters) throws GowallaRequestException {
		OutputStreamWriter out = null;
		
		URL url = null;
		HttpURLConnection conn = null;
		try {
	        url = new URL(createPostURL(resource, authentication)); 
	        conn = (HttpURLConnection)url.openConnection();
	        conn.setRequestMethod("POST");
	        appendRequestHeaders(conn, headers);
	        appendRequestHeaders(conn, authentication.getHeaders());
	        conn.setDoInput(true);
	        conn.setDoOutput(true);
	        conn.setUseCaches(false);
	        
	        final StringBuilder content = appendRequestParameters(new StringBuilder(), parameters);
	        appendRequestParameters(content, authentication.getParameters());
	        conn.addRequestProperty("Content-length", String.valueOf(content.length()));
	        conn.addRequestProperty("Content-Type", "application/x-www-form-urlencoded");

	        out = new OutputStreamWriter(conn.getOutputStream());
	        out.write(content.toString());
	        out.flush ();
	        
	        final String redirect = conn.getHeaderField("Location");
	        if(redirect != null) {	        	
	        	return getResource(redirect, authentication, null);
	        } else {
	        	return readResponse(conn);
	        }
		} catch(MalformedURLException e) {
			throw new GowallaRequestException("Malformed URL: " + url);
		} catch(IOException e) { 
			handleConnectionError(url, conn, e);
			return null;
		} finally {		
			if(out != null) {
				try { out.close(); } catch(Throwable t) {}
			}
		}
	}
	
	/**
	 * 
	 * @see com.ginsberg.gowalla.request.RequestHandler#getResource(java.lang.String, com.ginsberg.gowalla.auth.Authentication, java.util.Collection)
	 */
	@Override
	public String getResource(final String resource, final Authentication authentication, final Collection<RequestParameter> parameters) throws GowallaRequestException {	
		URL url = null;
		HttpURLConnection conn = null;
		try {
	        url = new URL(createGetURL(resource, authentication, parameters));
	        conn = (HttpURLConnection)url.openConnection();
	        appendRequestHeaders(conn, headers);
	        appendRequestHeaders(conn, authentication.getHeaders());
	        conn.setDoInput(true);
	        
	        final String redirect = conn.getHeaderField("Location");
	        if(redirect != null) {	        	
	        	return getResource(redirect, authentication, null);
	        } else {
	        	return readResponse(conn);
	        }
		} catch(MalformedURLException e) {
			throw new GowallaRequestException("Malformed URL: " + url);
		} catch(IOException e) { 
			handleConnectionError(url, conn, e);
			return null;
		} 
	}

	
	
	private void handleConnectionError(final URL url, final HttpURLConnection conn, Exception e) throws GowallaRequestException {
		// Sometimes details are in the body.
		ErrorResponse detail = null;
		try {
			detail = parseError(readErrorStream(conn));
		} catch(Throwable ignore) {
			// Ignore this.
		}
		try {
			switch(conn.getResponseCode()) {
				case HttpURLConnection.HTTP_FORBIDDEN: {
					if(conn.getHeaderField("Retry-After") != null) {
						throw new GowallaRequestException(errorString(String.format("Rate limited, try again after %s second(s)", conn.getHeaderField("Retry-After")), url, detail), e);
					}
					throw new GowallaRequestException(errorString("Forbidden:", url, detail), e);
				}
				case HttpURLConnection.HTTP_NOT_FOUND: {
					throw new RequestNotFoundException(errorString("Not Found:", url, detail), e);					
				}
				case HttpURLConnection.HTTP_NOT_ACCEPTABLE: {
					throw new RequestNotAcceptableException(errorString("Not Acceptable:", url, detail), e);
				}
				case HttpURLConnection.HTTP_BAD_REQUEST: {
					throw new GowallaRequestException(errorString("Bad request:", url, detail), e);
				}
				case HttpURLConnection.HTTP_UNAUTHORIZED: {
					throw new NotAuthorizedException(errorString("Invalid or missing credentials - check authentication for:", url, detail), e);
				}
				case HttpURLConnection.HTTP_UNAVAILABLE: {
					throw new ServiceUnavailableException(errorString("Service Unavailable (might be temorary):", url, detail), e);
				}
				default: {
					throw new GowallaRequestException(errorString("IOException: " + e.getMessage(), url, detail), e);
				}
			}
		} catch(IOException thisIsWhyWeCantHaveNiceThings) {
			throw new GowallaRequestException("IOException: " + e.getMessage(), e);
		}
	}
	
	/**
	 * Create the request URL.
	 */
	private String createGetURL(final String resource, final Authentication authentication, final Collection<RequestParameter> parameters) {
		final StringBuilder url = new StringBuilder();
		if(resource.startsWith("http")) {
			url.append(resource);
		} else {
			url.append(String.format("%s://%s%s", authentication.requiresSecure() ? "https" : "http",  host, resource));
		}
		final StringBuilder params = new StringBuilder();
		appendRequestParameters(params, parameters);
		appendRequestParameters(params, authentication.getParameters());
		if(params.length() > 0) {
			url.append("?").append(params);
		}
		return url.toString();
	}
	
	/**
	 * Create the request URL.
	 */
	private String createPostURL(final String resource, final Authentication authentication) {
		return String.format("%s://%s%s", authentication.requiresSecure() ? "https" : "http",  host, resource);
	}

	/**
	 * Append parameters to the buffer.
	 */
	private StringBuilder appendRequestParameters(final StringBuilder buf,  final Collection<RequestParameter> parameters) {
		if(parameters != null) {
			for(RequestParameter param : parameters) {
				if(buf.length() > 0) {
					buf.append("&");
				}
				buf.append(param.format());
			}
		}
		return buf;
	}
		
	/**
	 * Add request headers to the connection.
	 */
	private void appendRequestHeaders(final HttpURLConnection conn, final Collection<RequestHeader> headers) {
        for(RequestHeader header : headers) {
        	if(header != null) {
        		conn.addRequestProperty(header.getKey(), header.getValue());
        	}
        }
	}
	
	/**
	 * Read the standard response.
	 */
	private String readResponse(final HttpURLConnection conn) throws IOException {
		return readStream(conn.getInputStream());
	}
	
	/**
	 * Generic stream reader.
	 */
	private String readStream(final InputStream inputStream) throws IOException {
		BufferedReader in = null;
		final StringBuilder bodyResponse = new StringBuilder();
		try {
			in = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));

			String inputLine;

			while ((inputLine = in.readLine()) != null)  {
				bodyResponse.append(inputLine);
			}
		} finally {
			if(in != null) {
				try { in.close(); } catch(Throwable t) {}
			}
		} 
		return bodyResponse.toString();
	}
	
	/**
	 * Read the error stream.
	 */
	private String readErrorStream(final HttpURLConnection conn) throws IOException {
		return readStream(conn.getErrorStream());
	}
	
	/**
	 * If there was an error, parse it out.
	 */
	private ErrorResponse parseError(final String error) {
		if(error == null) {
			return null;
		}
		if(gson == null) {
			gson = new GsonBuilder().create();
		}
		try {
			return gson.fromJson(error, ErrorResponse.class);
		} catch(JsonParseException ignore) {
			// Well, fooey.
			return null;
		}
	}
	
	/**
	 * Format exception text.
	 */
	private String errorString(final String text, final URL url, final ErrorResponse err) {
		if(err != null) {
			return String.format("%s - %s [%s]", text, url, err);
		} else {
			return String.format("%s - %s [No Detail]", text, url);
		}
	}
	 
}
